寫完草稿的事後囉唆:寫一寫才發現python的import機制根本博大精深,本人hold不住,因此只寫了些比較不入流的技術文,請各位見諒哈哈
用過python的人誰不知道import是什麼呢?就跟C++的include或是Nodejs的require一樣,都是用來載入標準模組或者是第三方模組的有力工具,但你確定你真的知道怎麼好好的使用import嗎,當要開發一個比較大型的開發模組或是你個人的工具箱時,為了去耦合性與擴充性,import是不能隨便亂使用的,以下的import雜談系列,就以個人極粗淺的開發經驗,拋磚引玉的說明一下import常有的坑,還有import的一些基礎和進階用法。
其實python的import機制是這個語言裏面較為複雜的一部份,在剖析這機制裡的技術細節時,先來思考一下究竟開發者或使用者對於import有什麼實際的功能需求,讓python的import機制如此複雜,我們可以從兩部份來做發想:
1.被import的對象,就是指模組本身
2.import別人的對象,也就是引入模組的程式碼
今天先以模組的角度來做思考,我們知道模組的組成有可能只是一個py檔,也有可能是一個由資料夾所組成的package,這個package可能是長這樣:
main_module/
├── __init__.py
├── sub_module1/
| ├── __init__.py
| ├── ex1_1.py
| └── ex1_2.py
|
├── sub_module2/
| ├── __init__.py
| ├── sub_module2_1/
| | ├── __init__.py
| | ├── ex2_1_1.py
| | └── ex2_1_2.py
| |
| ├── ex2_1.py
| └── ex2_2.py
|
├── sub_module3/
| ├── ex3_1.py
| └── ex3_2.py
|
├── ex0_1.py
├── ex0_2.py
└── some_data.data
議題一:當我們在建構一個package會出現一個議題,那就是當sub_module1裡的ex1_1.py想要去import位在sub_module2裡的ex2_1.py,我們要用絕對路徑去import還是用相對路徑呢?
如果我們是用絕對路徑去import會出現一個維護性的問題:
In ex1_1.py:
import main_module.sub_module2.ex2_1
這個維護性的問題為,這方法把最頂層的module名稱寫死了,如果有一天我們發現main_module的名稱不太適合,想要更改一下,但是會導致這module裏面所有有用absolute import的py檔都需要重新更改路徑名稱,當程式愈長愈大,要改頂層的module愈會產生很大的麻煩。
所以python有提供一個相對路徑import(relative import),其方法如下:
In ex1_1.py:
import ..sub_module2.ex2_1 # ..回溯到上一層路徑,也就是main_module/
from .. import sub_module2.ex2_1 # 這句與上一句同義
如果這個想要回溯上兩層路徑的話,比如說sub_module2_1裡的ex2_1_1.py想要去import位於sub_module3的ex3_1.py:
In ex2_1_1.py:
import ...sub_module3.ex3_1.py
議題二:既然import可以支援相對路徑,而我們直覺上也希望python可以正確讀取相對路徑字串,像是'.'代表的是這個py檔目前所在目錄,但實際上這是行不通的,比如說位於main_module的ex0_1.py希望讀取位於同一個資料夾的some_data.data,但沒辦法用像是open('./some_data.data','r').read()這種相對路徑的方式去open他,這看起來不合我們的直覺,這是為什麼呢?
在思考這個原因之前,先來觀察python實際上的行為:
In ex0_1.py:
import os
import os.path
print(os.path.abspath('.')) # 用os.path模組來查看這個相對路經的起始目錄是否是我們所預期的
data = open('./some_data.data','r').read()
In main_module/../test.py(想要使用main_module裏面的ex0_1的外部檔案):
from main_module import ex0_1
In bash(位於main_module/../):
$ python3 test.py
/home/shnovaj30101/note/python/contest # 這個路徑是位於"main_module/../",正好是執行檔所在目錄
Traceback (most recent call last):
File "test.py", line 1, in <module>
from main_module import ex0_1
File "/home/shnovaj30101/note/python/contest/main_module/ex0_1", line 4, in <module>
data = open('./some_data.data','r').read()
FileNotFoundError: [Errno 2] No such file or directory: './some_data.data'
發現在ex0_1.py檔裏面的相對路徑'.'並不代表這個ex0_1.py檔目前所在目錄,而是執行檔test.py所在目錄,也就是說當ex0_1.py處在被import的角色時,他完全不能決定究竟'.'代表的是什麼路徑,那我們要如何在ex0_1.py中成功取得這個ex0_1.py當下所在的目錄,或是退而求其次,取得整個模組main_module所在路徑也行?
雖然python的'.'是被設定在執行檔的工作目錄,但python還是有一些內置變數紀錄了module檔案本身(比如說ex0_1.py)或是最上層的整體package(比如說main_module)的資訊:
(1) __package__:這變數紀錄了整體package的資訊
(2) __file__:這變數紀錄了module檔案本身的資訊
如果想要獲取當下所在的目錄或是整體package的路徑只要使用os.path.abspath()就行了:
In ex0_1.py:
import os
import os.path as path
print(os.path.abspath('.'))
print(os.path.abspath(__file__))
print(os.path.abspath(__package__))
data = open(os.path.join(os.path.dirname(os.path.abspath(__file__)),'foo.py'),'r').read() # 阿...似乎挺長
In main_module/../test.py(想要使用main_module裏面的ex0_1的外部檔案):
from main_module import ex0_1
In bash(位於main_module/../):
$ python3 test.py # 沒有出error代表open成功
/home/shnovaj30101/note/python/contest # 執行檔位置
/home/shnovaj30101/note/python/contest/main_module/ex0_1.py # 檔案本身位置
/home/shnovaj30101/note/python/contest/main_module # package的位置
仔細想想也還是很有道理的,關於議題一的相對路徑import屬於package內部互相調用,是module自己的事,所以import可以直接的用'.'符號來代表檔案本身路徑,但在議題二,當ex0_1.py被調用,呼叫指令的主體其實是執行檔,所以'.'符號指的當然是執行檔的工作目錄。
好吧其實我也還沒搞懂內部細節在幹嘛,只是從結果自圓其說方便記憶,反正也是可以從__file__和__package__來取得模組的路徑資訊,但如果覺得用__file__和__package__來取的資料很不簡潔的話,python cookbook有提供一個很好的解法:http://python3-cookbook.readthedocs.io/zh_CN/latest/c10/p08_read_datafile_within_package.html
開始有點詞窮了嗚嗚,今天先這樣,把一些份分到明天。